    org 10000h

    jmp     short Label_Start
%include    "fat12.inc"

BaseOfKernel            equ 0x00        ;内核的基址
OffsetOfKernel          equ 0x100000    ;内核的偏移

BaseTmpOfKernelAddr     equ 0x00        ;内核在引导程序的时候的临时基址
OffsetTmpOfKernel       equ 0x7E00      ;内核在引导程序的时候的临时偏移

MemoryStructBufferAddr  equ 0x7E00      ;这里是内核从1M以内空间转移到1M以后空间后，放参数的地方

[SECTION .gdt32]                  ;这下面是32位 GDT 结构段区域

LABEL_DESC_NULL32:      dd  0,0                     ;规定第一项必须是NULL描述符
LABEL_DESC_CODE32:      dd  0x0000FFFF,0x00CF9A00   ;这个是代码段描述符
LABEL_DESC_DATA32:      dd  0x0000FFFF,0x00CF9200   ;这个是数据段描述符

GdtLen32                equ $ - LABEL_DESC_NULL32   ;这个是 GDT 的长度
GdtPtr32                dw  GdtLen32 - 1              ;低2个字节是用来描述 GDT 表的长度
                        dd  LABEL_DESC_NULL32       ;高4个字节是用来描述 GDT 表的基地址

SelectorCode32          equ LABEL_DESC_CODE32 - LABEL_DESC_NULL32 ;代码段选择子，描述的是代码段选择子在GDT表中的索引号
SelectorData32          equ LABEL_DESC_DATA32 - LABEL_DESC_NULL32 ;数据段选择子

[SECTION .gdt64]                ;这下面是64位 GDT 结构段区域

LABEL_DESC_NULL64:      dq  0x0000000000000000
LABEL_DESC_CODE64:      dq  0x0020980000000000
LABEL_DESC_DATA64:      dq  0x0000920000000000

GdtLen64                equ $ - LABEL_DESC_NULL64
GdtPtr64                dw  GdtLen64 - 1
                        dd  LABEL_DESC_NULL64

SelectorCode64          equ LABEL_DESC_CODE64 - LABEL_DESC_NULL64
SelectorData64          equ LABEL_DESC_DATA64 - LABEL_DESC_NULL64

[SECTION .code16]               ;这里意思是下面的代码编译到同一快连续的内存上
[BITS 16]                       ;这里是告诉编译器下面代码编译的时候，要在16位状态下，详细建议百度
    
Label_Start:
;1. 初始化段寄存器
        mov     ax,     cs
        mov     ds,     ax
        mov     es,     ax
        mov     ax,     0x00
        mov     ss,     ax
        mov     sp,     0x7c00      ;将栈顶指向 SS:0x7c00

;2. 显示提示信息
        push    dx
        push    cx
        push    bp
        mov     dx,     0100h
        mov     cx,     12
        mov     bp,     StartLoaderMessage
        call    Func_ShowStrGreen
        pop     bp
        pop     cx
        pop     dx

;3. 因为实地址模式下只能访问1MB地址空间，所以下面的代码是打破这个限制，使用的是开启 A20
        push    ax
        in      al,     92h         ;这个92h是A20快速门的I/O端口号，这里把92H端口数据取出来放到了 AL 里面了
        or      al,     2h          ;这里是把 AL 的第1位置为 1 
        out     92h,    al          ;这里把处理好的数据赋值给 92H I/O端口
        pop     ax

        cli                         ;关中断
    
        db      0x66                ;下面要使用32位宽数据指令
        lgdt    [GdtPtr32]          ;用来加载保护模式结构数据信息

        mov     eax,    cr0         ;
        or      eax,    1           ;CR0的第0位就是保护模式的标志位，可以看 Intel 的白皮书
        mov     cr0,    eax

        mov     ax,     SelectorData32
        mov     fs,     ax          ;改变FS段寄存器的值，使CPU可以访问4G的内存空间

;4. 关闭保护模式，回到实地址模式
        mov     eax,    cr0
        and     al,     0feh        ;将第0位置为0
        mov     cr0,    eax

        sti                         ;开中断

;5. 下面的代码是查找 Kernel 文件的
        mov     word[SectorNo], SectorNumOfRootDirStart ;将根目录的起始扇区号，>

;1> 从根目录占用的扇区开始查找，每次都换下一个扇区
Label_Search_In_Root_Dir_Begin:
        cmp     word[RootDirSizeForLoop], 0 ;根目录占用的扇区数与0比较，如果等于
        jz      Label_No_KernelBin
        dec     word[RootDirSizeForLoop]    ;将根目录占用的扇区数减去
        mov     ax,     00h
        mov     es,     ax                  ;将ES改成00h
        mov     bx,     8000h               ;将BX改为8000H，ES:BX=接受软盘数据的数据缓冲
        mov     ax,     [SectorNo]          ;通过AX传参
        mov     cl,     1                   ;通过CL传参
        call    Func_ReadOneSector
        mov     si,     KernelFileName      ;将内核文件名称地址传入SI
        mov     di,     8000h
        cld                                 ;告诉CPU现在可以从前往后对比了
        mov     dx,     10h                 ;每个扇区可容纳的目录项个数(512/32=16=0x10)

;2> 开始在这个扇区中查找内核文件，这里每次换个目录项
Label_Search_For_KernelBin:
        cmp     dx,     0
        jz      Label_Goto_Next_Sector_In_Root_Dir  ;如果这个扇区没有找到，就下>
        dec     dx
        mov     cx,     11      ;目录项的文件名长度为11B，这里是十进制的

;3> 这里是对比文件的名称
Lable_Cmp_FileName:
        cmp     cx,     0
        jz      Label_FileName_Found    ;如果找到，就提示，并进入到下个步骤
        dec     cx
        lodsb                       ;将 SI 指向的值放到 AL 里面，只有一个字节

        cmp     al,     byte[es:di] ;比较字节是否一致
        jz      Label_Go_On         ;如果一致，就继续比较下一个字节
        jmp     Label_Different     ;如果不一样，就换下个目录项

;4> 当第一个字符一致的时候，就继续回去比较下个字符
Label_Go_On:
        inc     di                  ;更换为下个字节
        jmp     Lable_Cmp_FileName  ;跳回去，继续比较

;5> 当第一个字符不一致的时候，就会换下个目录项
Label_Different:
        and     di,     0ffe0h              ;0ffe0h=0b1111111111100000，将DI的低5位清零>
        add     di,     20h                 ;这里是因为一个目录项大小为32B，十六进制就是
        mov     si,     KernelFileName      ;将文件的地址重新传给 SI 从头开始比较
        jmp     Label_Search_For_KernelBin  ;继续回去比较

;6> 当这个扇区没有找到的时候，就换下个扇区
Label_Goto_Next_Sector_In_Root_Dir:
        add     word[SectorNo], 1   ;换下一个扇区
        jmp     Label_Search_In_Root_Dir_Begin

;6. 这段代码是显示没有找到 Kernel 程序文件的错误信息，然后就让CPU停在这里
Label_No_KernelBin:
;这里没有保存现场是因为不需要，我们就是不让运行了，让用户重新开机
        mov     dx,     0300h
        mov     cx,     56
        mov     bp,     NoKernelMessage
        call    Func_ShowStrGreen
        jmp     $

;7. 找到 Kernel 程序后，把它加载到内存中，位置已经被指定了
Label_FileName_Found:
        mov     ax,     RootDirSectors  ;RootDirSectors -> 根目录占用的扇区数
        and     di,     0ffe0h          ;直接来到条目的开始
        add     di,     01ah            ;1ah=26，就是将 DI 指向起始簇号，这里由>
        mov     cx,     word[es:di]     ;将起始簇号暂时保存到 CX 里面
        push    cx                      ;将起始簇号暂时保存到栈里面
        add     cx,     ax              ;根目录占用的扇区数 + 起始簇号 = 文件的>
        add     cx,     SectorBalance   ;文件的磁盘起始扇区号版本1 + 扇区保留 = 
        mov     eax,    BaseTmpOfKernelAddr     ;将 Kernel 程序的临时基址放到 ax 中 
        mov     es,     eax                     ;再将 Kernel 程序的基址最终赋值给 ES
        mov     bx,     OffsetTmpOfKernel       ;再将 Kernel 程序的临时偏移赋值给 BX 这两步>
        mov     ax,     cx              ;准备使用软盘读取函数了

;8. 开始从软盘读取文件出来，然后再换到真正的地址
;1> 下面这段代码是用来显示一个 . 用的是 int 10h 和 AH = 0EH 的中断程序
Label_Go_On_Loading_File:
        push    ax
        push    bx
        mov     ah,     0eh
        mov     al,     '.'
        mov     bl,     0fh
        int     10h
        pop     bx
        pop     ax

;2> 把文件从软盘里面读出来，每次只读一个扇区
        mov     cl,     1               ;这里是只读取一个扇区
        call    Func_ReadOneSector
        pop     ax                      ;AX当前值是文件的起始簇号

;3> 保存当前的现场环境，这里要使用到两个段寄存器，选择的是 FS 和 DS
        push    cx
        push    eax
        push    fs                      ;复制需要两个段寄存器
        push    edi
        push    esi
        push    ds

;4> 把内核真正要放的地址准备好
        mov     cx,     200h            ;400h=1024，这个是两个扇区的大小
        mov     ax,     BaseOfKernel    ;把内核的基址传给 AX
        mov     fs,     ax              ;通过 AX 作为中间寄存器
        mov     edi,    dword[OffsetOfKernelCount]

;5> 把刚才从软盘里面拿出来的内核放的地方准备好
        mov     ax,     BaseTmpOfKernelAddr
        mov     ds,     ax
        ;mov     esi,    dword[cs:OffsetTmpOfKernelCount]
        mov     esi,    OffsetTmpOfKernel

;6> 使用 loop 指令来一个一个字节的复制
Label_Move_Kernel:
        mov     al,     byte[ds:esi]
        mov     byte[fs:edi],    al

        inc     esi
        inc     edi
        
        loop    Label_Move_Kernel

;7> 将内核移动的地址新的偏移更新到变量里面，然后恢复现场
        pop     ds
        mov     dword[OffsetOfKernelCount], edi
        ;mov     dword[OffsetTmpOfKernelCount], esi
        pop     esi
        pop     edi
        pop     fs
        pop     eax
        pop     cx

;8> 获取到内核文件的下一个簇号，判断簇号是否是最后一个，如果是就跳出循环，否则继续
        call    Func_GetFATEntry
        cmp     ax,     0FFFh           ;这里是判断是否是最后一个簇，FAT表项中 F
        jz      Label_File_Loaded
        push    ax
        mov     dx,     RootDirSectors  ;这里重置 AX 为根目录占用的扇区数
        add     ax,     dx
        add     ax,     SectorBalance   ;重新计算机出文件的磁盘起始扇区号
        ;add     bx,     [BPB_BytesPerSec]   ;指向下一段未使用的内存，说白了就是>
        mov     bx,     OffsetTmpOfKernel
        jmp     Label_Go_On_Loading_File

;9. 提示内核已经加载完毕
Label_File_Loaded:
        mov     ax,     0B800h
        mov     gs,     ax
        mov     ah,     0Fh
        mov     al,     'G'
        mov     [gs:((80 * 0 + 39) * 2)], ax

;10. 此时软盘已经没有用了，就可以把软盘关闭了
Label_Kill_Motor:
        push    dx
        push    ax
        mov     dx,     03F2h
        mov     al,     0
        out     dx,     al
        pop     ax
        pop     dx

;11. 提示要开始获取内存结构了，并且为获取内存结构做准备工作
        mov     dx,     0400h
        mov     cx,     24
        mov     bp,     StartGetMemStructMessage
        call    Func_ShowStrGreen

        mov     ebx,    0                   ;ebx必须为0，这是规定，可以百度int 15h
        mov     ax,     0x00                ;段寄存器为0x00
        mov     es,     ax
        mov     di,     MemoryStructBufferAddr  ;指定要读取的内存结构数据放的偏移

;12. 通过 int 15h 和 eax = 0x0E820 来获取内存结构，关于这个中断程序，可以直接百度一下了解详情
Label_Get_Memory_Struct:
        mov     eax,    0E820h      ;这个是固定值，上面已经说了为什么了
        mov     ecx,    20          ;ecx不管是不是20，BIOS也会填充20个字节的
        mov     edx,    0534D4150h  ;edx 0534D415h--BIOS将会使用此标志，对调用者将要请求的系统映像信息进行检验，这些信息会被BIOS放置到es:di所指向的结构中
        int     15h
        jc      Label_Get_Memory_Fail   ;如果 int 15h 成功的话，CF标志位就是0，反之就是 1
        add     di,     20          ;继续填充下20个字节
        inc     dword[MemStructNumber]
        cmp     ebx,    0           ;如果ebx为0说明它是最后一个地址范围描述符
        jne     Label_Get_Memory_Struct
        jmp     Label_Get_Memory_OK

;13. 如果读取完成，就过来提示一下
Label_Get_Memory_OK:
        mov     dx,     0600h
        mov     cx,     29
        mov     bp,     GetMemStructOKMessage
        call    Func_ShowStrGreen

;获取VBE控制器信息
        mov     ax,     0x00
        mov     es,     ax          ;给es一个值，这个是硬性要求
        mov     di,     0x8000      ;RS:DI指向VbeInfoBlock信息块结构体的起始地址
        mov     ax,     4F00h       ;功能号：获取VBE控制器信息
        int     10h

        cmp     ax,     004Fh
        jnz     Label_Get_SVGAVBE_Fail

        mov     ax,     0x00
        mov     es,     ax
        mov     si,     0x800e

        mov     esi,    dword[es:si]    ;把 VideoModeList 指针放到 esi 里面
        mov     edi,    0x8200          ;下面获取VBE模式信息需要用到di

Label_SVGA_Mode_Info_Get:

        mov     cx,     word[es:esi]    ;把 VideoModeList 指向的第一个字节读出来

        push    ax

        mov     ax,     00h
        mov     al,     ch
        call    Func_DisplayAL

        mov     ax,     00h
        mov     al,     cl
        call    Func_DisplayAL

        pop     ax                      ;前面的一段代码是将 读取出来的 cx 打印出来

        cmp     cx,     0FFFFh              ;判断是否到最后一个了
        jz      Label_SVGA_Mode_Info_Finish

        mov     ax,     4F01h               ;获取VBE模式信息的功能号
        int     10h

        cmp     ax,     004Fh
        jnz     Label_SVGA_Mode_Info_FAIL

        inc     dword[SVGAModeCounter]      ;增加计数
        add     esi,    2                   ;换到下一个字节
        add     edi,    0x100               ;把存放VBE模式信息的起始地址加上 0x100

        jmp     Label_SVGA_Mode_Info_Get

Label_SVGA_Mode_Info_Finish:
        mov     dx,     0500h
        mov     cx,     23
        mov     bp,     GetSVGAModeInfoErrMessage
        call    Func_ShowStrGreen

;14. 设置 VBE 的显示模式
        mov     ax,     4F02h       ;设置 VBE 显示模式的功能号
        mov     bx,     4180h       ;这个设置的是 0x180 模式，也就是1440x900
        int     10h

        cmp     ax,     004Fh
        jnz     Label_Set_SVGA_Mode_VESA_VBE_Fail

;15. 初始化 GDT 和 IDT，然后进入到保护模式，保护模式只是一个中间过程而已，所以不需要准备过多东西
;1> 关中断，进入保护模式的过程中不允许出现中断和异常，这里没有关闭可屏蔽中断
        cli                         ;关中断

;2> 载入 GDT 表
        db      0x66
        lgdt    [GdtPtr32]

;3> 载入 IDT 表
        db      0x66
        lidt    [IDT_POINTER]

;4> 将 CR0 的PE位置为1，来开启保护模式
        mov     eax,    cr0
        or      eax,    1
        mov     cr0,    eax

;5> 跳到保护模式临时代码区
        jmp     dword SelectorCode32:GO_TO_TMP_Protect

;如果读取内存结构出错，就会来到这里提示一下
Label_Get_Memory_Fail:
        mov     dx,     0500h
        mov     cx,     23
        mov     bp,     GetMemStructErrMessage
        call    Func_ShowStrGreen
        jmp     $

;如果设置 SVGA 模式错误了，就会来到这里提示
Label_Set_SVGA_Mode_VESA_VBE_Fail:
        mov     dx,     0500h
        mov     cx,     25
        mov     bp,     SetSVGAModeFailMessage
        call    Func_ShowStrGreen
        jmp     $

;如果获取 VBE 控制器信息失败，就会提示
Label_Get_SVGAVBE_Fail:
        mov     dx,     0500h
        mov     cx,     23
        mov     bp,     GetSVGAModeInfoErrMessage
        call    Func_ShowStrGreen
        jmp     $

Label_SVGA_Mode_Info_FAIL:
        mov     dx,     0500h
        mov     cx,     23
        mov     bp,     GetSVGAModeInfoErrMessage
        call    Func_ShowStrGreen
        jmp     $

[SECTION .code32]               ;这段代码是告诉编译器状态为32位宽
[BITS 32]
;17. 进入临时保护模式
GO_TO_TMP_Protect:
;1> 初始化段寄存器和栈指针
        mov     ax,     0x10
        mov     ds,     ax
        mov     es,     ax
        mov     fs,     ax
        mov     ss,     ax
        mov     esp,    7E00h

;2> 检测 CPU 是否支持 IA-32e 模式，也就是长模式
        call    Func_Support_Long_Mode
        test    eax,    eax             ;判断是否为空

        jz      Label_No_Support        ;为空就说明不支持，所以这里会跳转

;18. 初始化临时的页表，把页目录首地址设置在0x90000地址处
        mov     dword[0x90000], 0x91007     ;第一级结构
        mov     dword[0x90800], 0x91007

        mov     dword[0x91000], 0x92007     ;第二级结构

;下面是第三级结构
        mov     dword[0x92000], 0x000083
        mov     dword[0x92008], 0x200083
        mov     dword[0x92010], 0x400083
        mov     dword[0x92018], 0x600083
        mov     dword[0x92020], 0x800083
        mov     dword[0x92028], 0xA00083

;19. 载入 GDTR，把临时的 GDT 表通过 LGDT 加载到 GDTR 寄存器中，然后初始化段寄存器和栈指针
        db      0x66
        lgdt    [GdtPtr64]
        mov     ax,     0x10
        mov     ds,     ax
        mov     es,     ax
        mov     fs,     ax
        mov     gs,     ax
        mov     ss,     ax

        mov     esp,    7E00h

;20. 开启 PAE 功能，它是物理地址扩展
        mov     eax,    cr4
        bts     eax,    5       ;bts会把 EAX 的第五位先存储到 CF 位，然后再把它置为 1
        mov     cr4,    eax

;21. 载入 CR3 寄存器，把上面构造的页表的页目录基址放入
        mov     eax,    0x90000
        mov     cr3,    eax

;22. 通过置位 IA32_EFER 寄存器的第8位，也就是 LME 标志位，来激活 IA-32e 模式
        mov     ecx,    0C0000080h      ;0C0000080h它是 IA32_EFER 寄存器的编码
        rdmsr                           ;建议百度 RDMSR 和 WRMSR 的详细用处

        bts     eax,    8               ;第20步里面有讲解
        wrmsr

;23. 最后还要开启保护模式和分页机制
        mov     eax,    cr0
        bts     eax,    0               ;第0位是 PE 位
        bts     eax,    31              ;第31位是 PG 位
        mov     cr0,    eax

;24. 跳转到内核，把 CPU 控制权交给内核程序
        jmp     SelectorCode64:OffsetOfKernel

;提示不支持 IA-32e 模式，并将程序停在这里
Label_No_Support:
        mov     dx,     0500h
        mov     cx,     43
        mov     bp,     NoSupportIA32EMessage
        call    Func_ShowStrGreen
        jmp     $

;32位的函数：用来查看是否支持 IA-32e 模式（长模式）
;参数：无
;返回值：EAX（只有 0 和 1 两种结果）
Func_Support_Long_Mode:
;1> 保存现场
        push    eax
        push    edx

;2> 通过 CPUID 和 eax = 0x80000000 来获取CPU的扩展信息输入最大值
        mov     eax,    0x80000000
        cpuid

;3> 判断返回的信息（CPU的扩展功能号）是否大于 0x80000001，
;如果大于说明有可能支持，反之就不可能支持的
        cmp     eax,    0x80000001
        setnb    al                                      ;如果 CF 标志位为1，那么就会把 AL 设置为1，反之设置为0
        jb      Label_Support_Long_Mode_Done

;4> 通过 CPUID 和 eax = 0x80000001 来获取CPU的特征和扩展特征位
        mov     eax,    0x80000001
        cpuid
        bt      edx,    29          ;把 EDX 的29位的值传给 CF 标志位
        setc    al                  ;如果 CF=1，就把 AL 置为 1

;5> 检测玩后就返回到调用处
Label_Support_Long_Mode_Done:
        movzx   eax,    al          ;用来把 AL 扩展成32位后，再重新赋值给 EAX，例：AL = 1，EAX = 50，执行完后是 EAX = 1

;6> 恢复现场，并返回
        pop     edx
        pop     eax
        ret


;函数区域
[SECTION .code16lib]
[BITS 16]

;函数：软盘读取函数
;参数为：AX传入待读取的磁盘起始扇区号
;        CL传入的是读入的扇区数量
;        ES:BX设置为目标缓冲区起始地址
Func_ReadOneSector:
        push    bp
        push    dx
        mov     bp,     sp
        sub     esp,    2  ;为函数开辟了2字节的临时内存空间
        mov     byte[bp - 2], cl    ;将读入的扇区数量放到临时内存里面
        push    bx
        mov     bl,     [BPB_SecPerTrk]
        div     bl          ;待读取的磁盘起始扇区号 / 每磁道扇区数（Sector/track）
        inc     ah          ;求的余数自增1, 起始扇区号=余数 + 1，起始扇区号是从1开始的
        mov     cl,     ah  ;将求到的起始扇区号放到 CL 里面
        mov     dh,     al  ;将求的商（前7位是磁道号，后1位是磁头号）放到 DH 里>
        shr     al,     1   ;将商（磁头号）右移1位，得到磁道号
        mov     ch,     al  ;将磁道号放到 CH 里面
        and     dh,     1   ;将 DH 除了最后一位全部置为 0,每个磁片有两个磁头，磁
        pop     bx
        mov     dl,     [BS_DrvNum] ;驱动器号

Label_Go_On_Reading:
        mov     ah,     2
        mov     al,     byte[bp - 2]    ;读入的扇区数
        int     13h
        jc Label_Go_On_Reading  ;如果数据读取成功的话，会把CF标志位复位，这里不>
        add     esp,    2
        pop     dx
        pop     bp
        ret

;函数：找到序号为 AX 的扇区在 FAT 中的条目
;参数为：AX 为 FAT表项号
;返回值：存放在 AX 里面，是文件的簇号
Func_GetFATEntry:
        push    es
        push    bx
        push    dx
        push    ax
        mov     ax,     00
        mov     es,     ax      ;这里是要读取软盘了，所以需要给ES一个值
        pop     ax
        mov     byte[Odd],  0   ;将奇偶标志复位
        mov     bx,     3
        mul     bx              ;AX = AX * 3 
        mov     bx,     2
        div     bx              ;AX = AX / 2，余数放到 DX 里面
        cmp     dx,     0       ;如果余数为0，说明是偶数
        jz      Label_Even
        mov     byte[Odd],  1   ;如果余数不为0，说明是奇数，此时将 Odd 置为 1
 
Label_Even:
        xor     dx,     dx      ;将 DX 清零
        mov     bx,     [BPB_BytesPerSec]   ;BPB_BytesPerSec是每个扇区的字节数
        div     bx              ;AX = AX / BPB_BytesPerSec，此时 AX 就是FAT表项>
        mov     bx,     8000h   ;BX原来的值，函数开始的时候就保存了，这里是为了>
        add     ax,     SectorNumOfFAT1Start    ;SectorNumOfFAT1Start是FAT起始扇
        mov     cl,     2       ;要读的扇区数量
        call    Func_ReadOneSector
        add     bx,     dx      ;BX + DX = 表项在内存中的地址，DX 放的是余数，余
        mov     ax,     [es:bx] ;将表项内容读取到 AX 里面
        cmp     byte[Odd],  1   ;判断是否奇偶
        jnz     Label_Even_2    
        shr     ax,     4       ;右移4位是因为只有12位是有效的，而奇数是前12位有

Label_Even_2:
        and     ax,     0fffh   ;将最高的4位清零，因为他们没有用，FAT表项占用12b
        pop     dx
        pop     bx
        pop     es
        ret


;函数：打印绿色的字符串
;参数为：dx 在哪一行，哪一列打印
;        cx 要打印的字符串的长度
;        bp 要打印的字符串的偏移
Func_ShowStrGreen:
    push    bp
    push    ax
    push    bx
    mov     ax,     1301h
    mov     bx,     0002h
    push    ax
    mov     ax,     ds
    mov     es,     ax
    pop     ax
    int     10h
    pop     bx
    pop     ax
    pop     bp
    ret

;函数：显示16进制的数字
;参数：AL 要显示的16进制数字
Func_DisplayAL:
;1> 保存现场
    push    ecx
    push    edx
    push    edi

;2> 做准备工作，要显示在什么位置，要显示的字符颜色，要显示的字符先显示哪四位，分几次显示
    mov     edi,    [DisplayPosition]   ;把字符的屏幕偏移值放到 EDI 里面
    mov     ah,     0Fh                 ;把字符的颜色属性值放到 AH 里面
    mov     dl,     al                  ;把 AL 存放到 DL 里面
    shr     al,     4                   ;AL 右移 4 位，说明DL只是保存 AL 的低四位
    mov     ecx,    2                   ;loop 循环的次数

;3> 这段代码是先把 AL 高四位清零，然后比较 AL 是不是要显示数字，
;如果是数字，就不用调转到 .1，直接加 '0' ，其实这里加的是 '0' 的编码，这样就可以直接显示出来数字了
.begin:
    and     al,     0Fh
    cmp     al,     9
    ja      .1
    add     al,     '0'
    jmp     .2

;4> 如果要显示的是字母，那么就先把 AL 减去10，然后加上 'A' 的编码，不减9的原因是加的是 'A' 的编码，这样就必须从0开始
.1:
    sub     al,     0Ah
    add     al,     'A'

;5> 把处理好的编码放到 [gs:edi] 里面，这里可以肯定是 ASCII 编码，看长度就知道了，然后把 edi 换到下下个地址
.2:
    mov     [gs:edi],   ax
    add     edi,    2                   ;到下下个地址

;6> 再把 DL 存放的后四位还给 AL，然后回到 .begin 处，继续把低四位显示出来
    mov     al,     dl
    loop    .begin

    mov     [DisplayPosition],  edi     ;更新 DisplayPosition

;7> 将现场复原
    pop     edi
    pop     edx
    pop     ecx

;8> 返回到调用这个函数的下一个地址
    ret

;临时 IDT 区域
IDT:
    times   0x50    dq  0
IDT_END:

IDT_POINTER:
                            dw  IDT_END - IDT - 1
                            dd  IDT

;临时变量区域
SectorNo                    dw  0               ;扇区号
RootDirSizeForLoop          dw  RootDirSectors  ;用来循环查询扇区的时候控制扇区数量
Odd                         db  0               ;奇偶标志
OffsetOfKernelCount         dd  OffsetOfKernel  ;内核文件偏移计数
OffsetTmpOfKernelCount      dd  OffsetTmpOfKernel

MemStructNumber             dd  0

SVGAModeCounter             dd  0

DisplayPosition             dd  0               ;显示在哪个坐标上

;提示字符串区域
StartLoaderMessage:         db  "Start Loader."
NoKernelMessage             db  "ERROR: No Kernel.bin Found, Please check the hard disk.!"
KernelFileName:             db  "KERNEL  BIN",0
StartGetMemStructMessage:   db  "Start get memory struct."
GetMemStructErrMessage:     db  "Get memory struct Error."
GetMemStructOKMessage:      db  "Get memory struct Successful!"
NoSupportIA32EMessage:      db  "Your computer does not support IA-32e mode."
SetSVGAModeFailMessage:     db  "Setting SVGA mode failed."
GetSVGAModeInfoErrMessage:	db	"Get SVGA Mode Info ERROR"
